package exploits

/*
	exploits.go

	This file is used to define routing rules that blocks common exploits.
	These include SQL injection, file injection, and other common attack patterns.
	It can also detect requests made by AI crawlers or bots based on their user-agent strings.

	Warning: This is not a complete security solution. Sometimes it might misfire and block legitimate requests.
	Use with caution and always monitor the logs.

	References:
	https://github.com/NginxProxyManager/nginx-proxy-manager/blob/bb0f4bfa626bfa30e3ad2fa31b5d759c9de98559/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf

*/

import (
	_ "embed"
	"net/http"
	"regexp"
	"strings"

	agents "github.com/monperrus/crawler-user-agents"
)

type ExploitsRequestResponseType int

const (
	ExploitRequestResponseTypeNotFound       ExploitsRequestResponseType = iota //Respond with 404 Not Found
	ExploitRequestResponseTypeForbidden                                         //Respond with 403 Forbidden
	ExploitRequestResponseTypeBadRequest                                        //Respond with 400 Bad Request
	ExploitRequestResponseTypeDropConnection                                    //Drop the connection without responding
	ExploitRequestResponseTypeCaptcha                                           //Present a captcha challenge
)

type Detector struct {
	CheckCommonExploits bool
	CheckAiBots         bool
	ExploitRespType     ExploitsRequestResponseType
}

func NewExploitDetector(CheckCommonExploits bool, CheckAiBots bool, ExploitRespType ExploitsRequestResponseType) *Detector {
	return &Detector{
		CheckCommonExploits: CheckCommonExploits,
		CheckAiBots:         CheckAiBots,
		ExploitRespType:     ExploitRespType,
	}
}

// CheckIsAttack checks if the request is an attack based on common exploits
// return true if the request is handled
func (d *Detector) CheckIsAttack(w http.ResponseWriter, r *http.Request) bool {
	path := r.URL.Path
	if strings.HasPrefix(path, "/.well-known/") {
		return false
	}
	if d.CheckCommonExploits && d.RequestContainCommonExploits(r) {
		return d.handleExploitResponse(w, r, d.ExploitRespType)
	}
	if d.CheckAiBots && d.RequestIsMadeByAiCrawlerOrBots(r) {
		return d.handleExploitResponse(w, r, d.ExploitRespType)
	}
	return false
}

// GetResponseStatusCodeFromResponseType converts the response type to HTTP status code
func (d *Detector) GetResponseStatusCode() int {
	respType := d.ExploitRespType
	switch respType {
	case ExploitRequestResponseTypeNotFound:
		return http.StatusNotFound
	case ExploitRequestResponseTypeForbidden:
		return http.StatusForbidden
	case ExploitRequestResponseTypeBadRequest:
		return http.StatusBadRequest
	default:
		return http.StatusForbidden
	}
}

func (d *Detector) handleExploitResponse(w http.ResponseWriter, r *http.Request, respType ExploitsRequestResponseType) bool {
	isHandled := true
	switch respType {
	case ExploitRequestResponseTypeNotFound:
		http.NotFound(w, r)
	case ExploitRequestResponseTypeForbidden:
		http.Error(w, "Forbidden", http.StatusForbidden)
	case ExploitRequestResponseTypeBadRequest:
		http.Error(w, "Bad Request", http.StatusBadRequest)
	case ExploitRequestResponseTypeDropConnection:
		// Drop the connection without responding
		hj, ok := w.(http.Hijacker)
		if ok {
			conn, _, err := hj.Hijack()
			if err == nil {
				conn.Close()
			}
		}

	case ExploitRequestResponseTypeCaptcha:
		// Present a captcha challenge
	}
	return isHandled
}

// RequestContainCommonExploits checks if the request contains common exploits
// such as SQL injection, file injection, and other common attack patterns.
func (d *Detector) RequestContainCommonExploits(r *http.Request) bool {
	query := r.URL.RawQuery
	userAgent := r.UserAgent()

	// Block SQL injections
	sqlInjectionPatterns := []string{
		`union.*select.*\(`,
		`union.*all.*select.*`,
		`concat.*\(`,
	}
	for _, pattern := range sqlInjectionPatterns {
		if match, _ := regexp.MatchString(pattern, query); match {
			return true
		}
	}

	// Block file injections
	fileInjectionPatterns := []string{
		`[a-zA-Z0-9_]=http://`,
		`[a-zA-Z0-9_]=(\.\.//?)+`,
		`[a-zA-Z0-9_]=/([a-z0-9_.]//?)+`,
	}
	for _, pattern := range fileInjectionPatterns {
		if match, _ := regexp.MatchString(pattern, query); match {
			return true
		}
	}

	// Block common exploits
	commonExploitPatterns := []string{
		`(<|%3C).*script.*(>|%3E)`,
		`GLOBALS(=|\[|\%[0-9A-Z]{0,2})`,
		`_REQUEST(=|\[|\%[0-9A-Z]{0,2})`,
		`proc/self/environ`,
		`mosConfig_[a-zA-Z_]{1,21}(=|\%3D)`,
		`base64_(en|de)code\(.*\)`,
	}
	for _, pattern := range commonExploitPatterns {
		if match, _ := regexp.MatchString(pattern, query); match {
			return true
		}
	}

	// Block spam
	spamPatterns := []string{
		`\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b`,
		`\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b`,
		`\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b`,
		`\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b`,
	}
	for _, pattern := range spamPatterns {
		if match, _ := regexp.MatchString(pattern, query); match {
			return true
		}
	}

	// Block user agents
	userAgentPatterns := []string{
		`Indy Library`,
		`libwww-perl`,
		`GetRight`,
		`GetWeb!`,
		`Go!Zilla`,
		`Download Demon`,
		`Go-Ahead-Got-It`,
		`TurnitinBot`,
		`GrabNet`,
	}
	for _, pattern := range userAgentPatterns {
		if match, _ := regexp.MatchString(pattern, userAgent); match {
			return true
		}
	}

	return false
}

func (d *Detector) RequestIsMadeByAiCrawlerOrBots(r *http.Request) bool {
	userAgent := r.UserAgent()
	if userAgent == "" {
		return false
	}

	aiBotPatterns := []string{
		`(?i)openai`,
		`(?i)chatgpt`,
		`(?i)gpt-?`,
		`(?i)claude`,
		`(?i)anthropic`,
		`(?i)perplexity`,
		`(?i)perplexitybot`,
		`(?i)bingbot`,
		`(?i)bingpreview`,
		`(?i)serpapi`,
	}

	for _, pattern := range aiBotPatterns {
		if match, _ := regexp.MatchString(pattern, userAgent); match {
			return true
		}
	}

	// Fallback: treat known crawlers as bots too
	return agents.IsCrawler(userAgent)
}
